/*
 * Copyright (C) 2018 Logos Technology Ltd.
 */

package com.logos.common.source.network;

import com.logos.common.BuildConfig;
import com.logos.common.source.network.callback.HttpRespondResultCallBack;
import com.logos.common.source.network.callback.MultiUploadFileProgressListener;
import com.logos.common.source.network.converter.CustomGsonConverterFactory;
import com.logos.common.source.network.interceptor.CacheInterceptor;
import com.logos.common.source.network.interceptor.DecorateHeaderInterceptor;
import com.logos.common.source.network.interceptor.DownloadFileInterceptor;
import com.logos.common.source.network.interceptor.LoggerFactory;
import com.logos.common.source.network.interceptor.LoggerInterceptor;
import com.logos.common.source.network.interceptor.NotNetWorkInterceptor;
import com.logos.common.source.network.interceptor.RequestComInterceptor;
import com.logos.common.source.network.interceptor.UploadFileInterceptor;
import com.logos.common.source.network.request.UploadFileProcessManager;
import com.logos.common.source.network.request.UploadFileRequestBody;
import com.logos.tool.AppConstants;
import com.logos.tool.file.StorageUtils;
import com.logos.tool.log.LogUtil;
import com.logos.tool.string.StringUtil;

import java.io.File;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.ObservableTransformer;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
import okhttp3.Cache;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.RequestBody;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import tech.linjiang.pandora.Pandora;

/**
 * @author Young Ken
 */

public class NetWorkClient {

    private static final String TAG = NetWorkClient.class.getName();
    private BaseServerApi mServerApi;
    private MediaType mMediaType = MediaType.parse("application/x-www-form-urlencoded");
    private MediaType mDataMediaType = MediaType.parse("multipart/form-data");

    private NetWorkClient(BaseServerApi serverApi) {
        mServerApi = serverApi;
    }

    /**
     * post network request
     *
     * @param url      url
     * @param map      map
     * @param callback callback
     */
    @SuppressWarnings("unchecked")
    public void post(String url, Map map, final HttpRespondResultCallBack callback) {
        RequestBody body = RequestBody.create(mMediaType, getParamsOrderByKey(map));
        mServerApi.post(url, body)
                .map(new BaseFunction<>(callback))
                .compose(observableTransformer())
                .subscribe(new BaseNetWorkSubscriber(callback));
    }

    /**
     * get network request
     *
     * @param url          url
     * @param runSubThread 子线程中运行
     * @param callback     callback
     */
    @SuppressWarnings("unchecked")
    public void get(String url, boolean runSubThread, HttpRespondResultCallBack callback) {
        mServerApi.get(url)
                .map(new BaseFunction<>(callback))
                .compose(observableTransformer(runSubThread))
                .subscribe(new BaseNetWorkSubscriber(callback));
    }

    /**
     * get network request
     *
     * @param url          url path
     * @param param        map
     * @param runSubThread 子线程中运行
     * @param callback     callback
     */
    @SuppressWarnings("unchecked")
    public void get(String url, Map<String, Object> param,
                    boolean runSubThread,
                    HttpRespondResultCallBack callback) {
        mServerApi.get(url, param)
                .map(new BaseFunction<>(callback))
                .compose(observableTransformer(runSubThread))
                .subscribe(new BaseNetWorkSubscriber(callback));
    }

    /**
     * upload image
     *
     * @param url      url
     * @param fileMap  map of File
     * @param param    map
     * @param callback call back
     * @param listener UploadFileProgressListener
     */
    @SuppressWarnings("unchecked")
    public void upload(String url, Map<String, File> fileMap,
                       Map<String, Object> param,
                       HttpRespondResultCallBack callback,
                       MultiUploadFileProgressListener listener) {
        mServerApi.upload(url, displayFile(fileMap, listener), generateRequestBody(param))
                .map(new BaseFunction<>(callback))
                .compose(observableTransformer())
                .subscribe(new BaseNetWorkSubscriber(callback));
    }

    /**
     * 下载
     *
     * @param url      url
     * @param callback callback
     */
    @SuppressWarnings("unchecked")
    public void download(String url, HttpRespondResultCallBack callback) {
        mServerApi.download(url)
                .map(new BaseFunction<>(callback))
                .compose(observableTransformer())
                .subscribe(new BaseNetWorkSubscriber(callback));
    }

    /**
     * upload image
     *
     * @param url      url
     * @param fileMap  map of File
     * @param callback call back
     * @param listener UploadFileProgressListener
     */
    @SuppressWarnings("unchecked")
    public void upload(String url, Map<String, File> fileMap,
                       HttpRespondResultCallBack callback,
                       MultiUploadFileProgressListener listener) {
        mServerApi.upload(url, displayFile(fileMap, listener))
                .map(new BaseFunction<>(callback))
                .compose(observableTransformer())
                .subscribe(new BaseNetWorkSubscriber(callback));
    }

    /**
     * put request
     *
     * @param url      url
     * @param map      map
     * @param callback HttpRespondResultCallBack
     */
    @SuppressWarnings("unchecked")
    public void put(String url, Map map, HttpRespondResultCallBack callback) {
        RequestBody body = RequestBody.create(mMediaType, getParamsOrderByKey(map));
        mServerApi.put(url, body)
                .map(new BaseFunction<>(callback))
                .compose(observableTransformer())
                .subscribe(new BaseNetWorkSubscriber(callback));
    }

    /**
     * delete request
     *
     * @param url      url
     * @param callback HttpRespondResultCallBack
     */
    @SuppressWarnings("unchecked")
    public void delete(String url, HttpRespondResultCallBack callback) {
        mServerApi.delete(url)
                .map(new BaseFunction<>(callback))
                .compose(observableTransformer())
                .subscribe(new BaseNetWorkSubscriber(callback));
    }

    /**
     * delete request
     *
     * @param url      url
     * @param param    param
     * @param callback HttpRespondResultCallBack
     */
    @SuppressWarnings("unchecked")
    public void delete(String url, Map<String, Object> param,
                       HttpRespondResultCallBack callback) {
        mServerApi.delete(url, param)
                .map(new BaseFunction<>(callback))
                .compose(observableTransformer())
                .subscribe(new BaseNetWorkSubscriber(callback));
    }

    private static String getParamsOrderByKey(Map<String, String> paramValues) {
        StringBuffer result = new StringBuffer();
        List<String> paramNames = new ArrayList<String>(paramValues.size());
        paramNames.addAll(paramValues.keySet());
        Collections.sort(paramNames);
        for (String paramName : paramNames) {
            if (!StringUtil.isEmpty(paramValues.get(paramName))) {
                if (StringUtil.isEmpty(result.toString())) {
                    result.append(paramName).append("=").append(paramValues.get(paramName));
                } else {
                    result.append("&").append(paramName).append("=").append(paramValues.get(paramName));
                }
            }
        }
        return result.toString();
    }

    /**
     * get ObservableTransformer
     *
     * @param <T> t
     * @return ObservableTransformer
     */
    private <T> ObservableTransformer<T, T> observableTransformer(final boolean runSubThread) {

        return new ObservableTransformer<T, T>() {

            @Override
            public ObservableSource<T> apply(Observable<T> upstream) {
                if (runSubThread) {
                    return upstream.subscribeOn(Schedulers.io());
                } else {
                    return upstream.subscribeOn(Schedulers.io())
                            .observeOn(AndroidSchedulers.mainThread());
                }
            }
        };
    }

    private <T> ObservableTransformer<T, T> observableTransformer() {

        return new ObservableTransformer<T, T>() {

            @Override
            public ObservableSource<T> apply(Observable<T> upstream) {
                return upstream.subscribeOn(Schedulers.io())
                        .observeOn(AndroidSchedulers.mainThread());
            }
        };
    }

    private HashMap<String, RequestBody> displayFile(Map<String, File> fileMap,
                                                     MultiUploadFileProgressListener listener) {

        final HashMap<String, RequestBody> params;
        final HashMap<String, RequestBody> uploadImageSize;
        if (fileMap.size() <= 0) {
            LogUtil.debug(TAG, "[displayFile] " + "upload no found file!");
            return null;
        }

        params = new HashMap<>();
        uploadImageSize = new HashMap<>();

        Iterator<Map.Entry<String, File>> iterator = fileMap.entrySet().iterator();
        Map.Entry<String, File> entry;

        while (iterator.hasNext()) {
            entry = iterator.next();
            final File file = entry.getValue();
            RequestBody body = RequestBody.create(mDataMediaType, file);
            uploadImageSize.put(file.getName(), body);
        }

        //init process manager
        UploadFileProcessManager manager = new UploadFileProcessManager(uploadImageSize, listener);
        Iterator<Map.Entry<String, File>> requestIterator = fileMap.entrySet().iterator();
        Map.Entry<String, File> fileEntry;
        while (requestIterator.hasNext()) {
            fileEntry = requestIterator.next();
            String key = fileEntry.getKey();
            String fileName = fileEntry.getValue().getName();
            RequestBody requestBody = uploadImageSize.get(fileName);
            UploadFileRequestBody uploadFileRequestBody = new UploadFileRequestBody(
                    fileName, requestBody, manager);
            params.put("" + key + "\"; filename=\"" + fileName, uploadFileRequestBody);
        }
        return params;
    }

    /**
     * 比如可以这样生成Map
     * Map 这里面放置上传数据的键值对。
     */
    private Map<String, RequestBody> generateRequestBody(Map<String, Object> requestDataMap) {
        Map<String, RequestBody> requestBodyMap = new HashMap<>();
        for (String key : requestDataMap.keySet()) {
            RequestBody requestBody = RequestBody.create(mDataMediaType,
                    requestDataMap.get(key) == null ? "" : String.valueOf(requestDataMap.get(key)));
            requestBodyMap.put(key, requestBody);
        }
        return requestBodyMap;
    }

    public static class Builder {

        private String mBaseUrl = "";
        private int mTimeOut = 30;
        private int mWriteTimeout = 10 * 60;
        private int mReadTimeout = 5 * 60;

        private BaseServerApi mServerApi;
        private Retrofit mRetrofit;
        private Retrofit.Builder mRetrofitBuilder;
        private OkHttpClient mOkHttpClient;
        private OkHttpClient.Builder mOkHttpClientBuilder;

        private RxJava2CallAdapterFactory mJavaCallAdapterFactory;
        private UploadFileInterceptor mUploadFileFactory;
        private DownloadFileInterceptor mDownloadFileFactory;

        private CustomGsonConverterFactory mCustomGsonConverterFactory;
        private CacheInterceptor mCacheFactory;

        public Builder(String url) {
            mRetrofitBuilder = new Retrofit.Builder();
            mOkHttpClientBuilder = new OkHttpClient.Builder();
            initDefaultConfig(url); // 加载默认配置
        }

        private void initDefaultConfig(String url) {
            baseUrl(url);
            addCustomGsonConverterFactory(CustomGsonConverterFactory.create());
            addRxJavaCallAdapterFactory(RxJava2CallAdapterFactory.createAsync());
        }

        /**
         * set base url
         *
         * @param url url
         * @return Builder
         */
        public Builder baseUrl(String url) {
            mBaseUrl = url;
            return this;
        }

        /**
         * set net time out
         *
         * @param timeOut time
         * @return Builder
         */
        public Builder timeOut(int timeOut) {
            this.mTimeOut = timeOut;
            return this;
        }

        /**
         * addCustomGsonConverterFactory
         *
         * @param factory factory
         * @return Builder
         */
        public Builder addCustomGsonConverterFactory(CustomGsonConverterFactory factory) {
            mCustomGsonConverterFactory = factory;
            return this;
        }

        /**
         * add RxJava adapter factory
         *
         * @param factory RxJavaCallAdapterFactory
         * @return Builder
         */
        public Builder addRxJavaCallAdapterFactory(RxJava2CallAdapterFactory factory) {
            mJavaCallAdapterFactory = factory;
            return this;
        }

        /**
         * add upload file factory
         *
         * @param factory UploadFileInterceptor
         * @return Builder
         */
        public Builder addUploadFileFactory(UploadFileInterceptor factory) {
            mUploadFileFactory = factory;
            return this;
        }

        /**
         * add download file factory
         *
         * @param factory DownloadFileInterceptor
         * @return Builder
         */
        public Builder addDownloadFileFactory(DownloadFileInterceptor factory) {
            mDownloadFileFactory = factory;
            return this;
        }

        /**
         * add cache
         *
         * @param factory CacheInterceptor
         * @return Builder
         */
        public Builder addCacheFactory(CacheInterceptor factory) {
            mCacheFactory = factory;
            return this;
        }

        /**
         * set OkHttpClient
         *
         * @param client OkHttpClient
         * @return Builder
         */
        public Builder client(OkHttpClient client) {
            this.mOkHttpClient = client;
            return this;
        }

        /**
         * add header
         *
         * @param headers map
         * @return Builder
         */
        public Builder addHeader(Map<String, String> headers) {
            mOkHttpClientBuilder.addInterceptor(new DecorateHeaderInterceptor(headers));
            return this;
        }

        private Cache provideCache() {
            File file = StorageUtils.getStorage();
            String path = file.getPath() + AppConstants.FILE_ROOT_NAME + "/cache/net";
            return new Cache(new File(path), 10240 * 1024);
        }

        /**
         * crate NetWorkClient
         *
         * @return NetWorkClient
         */
        public NetWorkClient build() {
            //默认第一个添加
            mOkHttpClientBuilder.addInterceptor(RequestComInterceptor.create());
            mOkHttpClientBuilder.addInterceptor(NotNetWorkInterceptor.create());

            if (mCustomGsonConverterFactory != null) {
                mRetrofitBuilder.addConverterFactory(mCustomGsonConverterFactory);
            }

            if (mUploadFileFactory != null) {
                mOkHttpClientBuilder.addInterceptor(mUploadFileFactory);
            }

            //add http cache
            if (mCacheFactory != null) {
                mOkHttpClientBuilder.cache(provideCache());
                mOkHttpClientBuilder.addInterceptor(CacheInterceptor.create());
            }

            if (mDownloadFileFactory != null) {
                mOkHttpClientBuilder.addInterceptor(mDownloadFileFactory);
            }

            mOkHttpClientBuilder.connectTimeout(mTimeOut, TimeUnit.SECONDS);
            mOkHttpClientBuilder.readTimeout(mReadTimeout, TimeUnit.SECONDS);
            mOkHttpClientBuilder.writeTimeout(mWriteTimeout, TimeUnit.SECONDS);

            if (BuildConfig.DEBUG) {
                LoggerInterceptor logInterceptor =
                        new LoggerInterceptor(LoggerFactory.create());
                logInterceptor.setLevel(LoggerInterceptor.Level.BODY);
                mOkHttpClientBuilder.addNetworkInterceptor(logInterceptor);
            }

            Pandora pandora = Pandora.get();
            if (pandora != null) {
                mOkHttpClientBuilder.addInterceptor(pandora.getInterceptor());
            }
            mOkHttpClient = mOkHttpClientBuilder.build();

            if (this.mJavaCallAdapterFactory != null) {
                mRetrofitBuilder.addCallAdapterFactory(mJavaCallAdapterFactory);
            }

            if (mBaseUrl != null) {
                mRetrofitBuilder.baseUrl(mBaseUrl);
            }

            mRetrofitBuilder.client(mOkHttpClient);
            mRetrofit = mRetrofitBuilder.build();
            mServerApi = mRetrofit.create(BaseServerApi.class);
            return new NetWorkClient(mServerApi);
        }
    }
}
